/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fjsei.yewu.inspect;

import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.querydsl.core.BooleanBuilder;
import graphql.relay.Connection;
import graphql.schema.DataFetchingEnvironment;
import io.camunda.tasklist.dto.Pagination;
import io.camunda.tasklist.dto.TaskList;
import lombok.extern.slf4j.Slf4j;
import md.cm.base.Companies;
import md.cm.flow.ApprovalStmRepository;
import md.cm.unit.Units;
import md.specialEqp.Report;
import md.specialEqp.ReportRepository;
import md.specialEqp.inspect.*;
import md.system.User;
import org.apache.http.client.CookieStore;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.fjsei.yewu.dto.oldtj.PostExampleDataDto;
import org.fjsei.yewu.dto.oldtj.ResponseADto;
import org.fjsei.yewu.filter.SimpleReport;
import org.fjsei.yewu.graphql.AfterValuesConnection;
import org.fjsei.yewu.graphql.DbPageConnection;
import org.fjsei.yewu.graphql.IdMapper;
import org.fjsei.yewu.graphql.MemoryListConnection;
import org.fjsei.yewu.index.IspEs;
import org.fjsei.yewu.input.IspCommonInput;
import org.fjsei.yewu.jpa.PageOffsetFirst;
import org.fjsei.yewu.pojo.UserTask;
import org.fjsei.yewu.resolver.Comngrql;
import org.fjsei.yewu.bpm.UserTaskListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
import org.springframework.data.elasticsearch.core.*;
//import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
//import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
//import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
//import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.http.*;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Type;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

//import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
//import static org.elasticsearch.index.query.QueryBuilders.matchPhraseQuery;

/**默认Unit模型的属性； graphQL接口;
 * 底下批处理方案还不是一种的。 注释掉的也是1个方案。
 * 每一个graphQL的Object对象type都需单独声明一个XxxController implements IdMapper<type> {} 否则Id都无法转换成Relay要的GlobalID的。
 * */

@Slf4j
@Controller
@Transactional
public class IspController extends Comngrql implements IdMapper<Isp> {
	private final Units units;
	private final Companies companies;
	private final TaskRepository tasks;
	private final IspRepository isps;
	private final ApprovalStmRepository  approvalStms;
	private final ReportRepository  reports;

	private final UserTaskListService userTaskListService;

	@Autowired
	private ElasticsearchOperations esTemplate;	 //原本连简易构造器模式注入全都没办法

	/* RestTemplate不适合用于有连接状态的session JSESSIONID=? 这类请求不好处理; 只好改用apache...CloseableHttpClient
	* */
	private final RestTemplate restTemplate;

	/** registry参数：IDEA报红却能运行成功，实际运行注入DefaultBatchLoaderRegistry;
     *  消除报红，测试平台Units没有报红 https://blog.csdn.net/zy103118/article/details/84523452
	 * 添加了 @ComponentScan({"md","org.fjsei.yewu"}) 以后: Units报红消除了。
	 * */
	public IspController(BatchLoaderRegistry registry, Units units, Companies companies, TaskRepository tasks, IspRepository isps, ApprovalStmRepository approvalStms,
						 ReportRepository reports, UserTaskListService userTaskListService) {
		this.units = units;
		this.companies = companies;
		this.tasks = tasks;
		this.isps = isps;
		this.approvalStms = approvalStms;
		this.reports = reports;
		this.userTaskListService = userTaskListService;
		//原来 this.restTemplate = restTemplateBuilder.build();
		CookieStore cookieStore = new BasicCookieStore();
		BasicClientCookie cookie = new BasicClientCookie("FJSEINEW_COD", "000");	//第三方服务授权访问不一样
		cookie.setVersion(0);
		cookie.setDomain("27.151.117.66");
		cookie.setPath("/");
		cookieStore.addCookie(cookie);
		//Apache HttpComponents 5.1 or higher, as of Spring 6.0.强制使用 HttpClient 5，
		//Spring 6 删除了 RestTemplate 对 Apache HttpClient 4 的支持，取而代之的则是 Apache HttpClient 5。
		//之前基于httpclient4自定义的ssl与http客户端相关代码编译不过了。然后我在研究怎么用apache的httpclient5来定义支持ssl的http客户端时突然发现，可以直接用new RestTemplate()访问https服务;
		this.restTemplate = new RestTemplate( );
	}

	/**
	 * 这看出比旧的kickstart包来讲:Spring for graphQL显现大优点了，可直接修改Type的属性转义取值。
	 * 接口函数的隐含可用参数Isp isp也能够直接注入了。
	 */
	@SchemaMapping(typeName="Isp", field="reps")
	public Connection reps(Isp isp, DataFetchingEnvironment env) {
		List<SimpleReport> reports= isp.getReps();
		//TODO:需排序?
		return new MemoryListConnection(new ArrayList<>(reports)).get(env);
	}
	//graphQL: Query{内的普通接口} 类比Type{底下的属性字段的接口伪函数}，后面是专门针对某一个对象的内省：也可传递参数函数接口，前者是没有附加前提的普通函数。
	public Connection<Isp> searchIsp数据库(@Argument String orderBy,@Argument Boolean asc,@Argument IspCommonInput where,@Argument Integer first,
									 @Argument String after,@Argument Integer last,@Argument String before, DataFetchingEnvironment env) {
		User user= checkAuth();
		if(user==null)   return null;
		//List<Report>  retSet= reportRepository.findAllByDataJsonKeyIs("$." + "字段2", "数值");
		DbPageConnection<Isp> connection=new DbPageConnection<>(env);
		int offset=connection.getOffset();
		int limit=connection.getLimit();
		Pageable pageable;
		if (!StringUtils.hasLength(orderBy))
			pageable = PageOffsetFirst.of(offset, limit);
		else
			pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));
		QIsp qm = QIsp.isp;
		BooleanBuilder builder = new BooleanBuilder();
		builder.and(qm.no.eq(where.getNo()));
		//List<Isp>  isps = new ArrayList<Isp>();     T类型标定：
		Slice<Isp> rpage= (Slice<Isp>)isps.findAll(builder,pageable);
		List<Isp> isps=(List<Isp>) rpage.toList();
		return connection.setListData(isps).get(env);
	}
	/**直接从ElasticSearch IspEs存储搜索 报告号的
	 * 等责任人派工状态的 就已经可以按no从IspES搜出来了。
	 * */
	@QueryMapping
	public Connection<IspEs> searchIsp(@Argument String orderBy,@Argument Boolean asc,@Argument IspCommonInput where,@Argument Integer first,
									 @Argument String after,@Argument Integer last,@Argument String before, DataFetchingEnvironment env) {
		User user= checkAuth();
		if(user==null)   return null;
		//List<Report>  retSet= reportRepository.findAllByDataJsonKeyIs("$." + "字段2", "数值");
		DbPageConnection<IspEs> connection=new DbPageConnection<>(env);
		int offset=connection.getOffset();
		int limit=connection.getLimit();
		Pageable pageable;
		if (!StringUtils.hasLength(orderBy))
			pageable = PageOffsetFirst.of(offset, limit);
		else
			pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));

		List<Query> listquerys = new ArrayList<Query>();
//		NativeQueryBuilder ntqBuilder= NativeQuery.builder();
		if( StringUtils.hasText(where.getNo()) )
			listquerys.add(Query.of(q -> q
					.matchPhrase(m -> m
							.field("no")
							.query(where.getNo()).slop(0)
					)
			));
//			ntqBuilder= ntqBuilder.withQuery(q -> q
//					.matchPhrase(m -> m
//							.field("no")
//							.query(where.getNo()).slop(0)
//					)
//			);

		NativeQueryBuilder  nativeQueryBuilder = NativeQuery.builder()
//				.withQuery(q -> q.bool(			//改成过滤器模式的 速度快点，但是SearchHit返回的score=1.0的。
				.withFilter(q -> q.bool(
								v->v.must(listquerys)
						)
				);
		NativeQuery query =nativeQueryBuilder.withPageable(pageable).build();
//		Query query = ntqBuilder.withPageable(pageable).build(); 	这个Query是import org.springframework.data.elasticsearch.core.query.Query;
		SearchHits<IspEs> searchHits = esTemplate.search(query, IspEs.class);
		List<SearchHit<IspEs>> hits=searchHits.getSearchHits();
		Iterable<IspEs> list= (List<IspEs>) SearchHitSupport.unwrapSearchHits(hits);
		//		String queryDslStr= Objects.requireNonNull(query.getQuery()).toString();
		String queryDslStr= Objects.toString(query.getFilter(),"空");		//改成过滤器模式的 query3.getQuery().toString();
		//若改成HibernateSearc自带的搜索API做的搜索模式，这里可以直接查到Isp的Entity对象, 而不是这里的IspEs还需要再查关系数据库。但此项功能也鸡肋，所以还是这个方式俩步走路。
		return connection.setListData((List<IspEs>) list).get(env);
	}
	@Deprecated
	@QueryMapping
	public Iterable<SimpleReport> getReportOfISP废弃(@Argument String id) {
		Detail detail = entityOf(id,Detail.class);
		Assert.isTrue(detail != null, "未找到detail:" + id);
		//isp.getReps().stream().count();     //.collect(Collectors.toSet())
		//isp.getReps().stream().forEach(t->t.toString());
		return detail.getIsp().getReps();
	}

	/**查当前登录用户UserTask ：user.id , user.所属用户组过滤，排序。
	 * 根据Isp.report主报告的stm流转状态机做过滤； #问题：iSPRepository历史报告中查找的？查询性能有问题？数据一致性+延时容忍性+搜索速度?IspEs模型？消息队列todoList?
	 * 【zeebe引入】之后，这些需要全部改掉，不是依赖关系数据库的状态机流转表的搜索和状态过滤，今后改造：需要从流程引擎服务获取用户任务列表了。
	 * 实际从camunda-tasklist-client-java绕道tasklist服务端再进入ES读取的。Task这个概念经过两道graphQL才送到前端，也就是两次映射。我这里是第二次Task模型适配。
	 * getTodos目前分页机制没用到:只能看前面20条。
	 * 【可能出现】新的用户任务在前端只能看见部分字段，有些字段会有滞后性需要再次刷新才能获取到UserTask全部字段信息。
	 * */
	@QueryMapping
	public Connection<UserTask> getTodos(@Argument String orderBy, @Argument Boolean asc, @Argument IspCommonInput where, @Argument Integer first, @Argument String after, @Argument Integer last, @Argument String before, DataFetchingEnvironment env) {
		User user= checkAuth();
		if(user==null)   return null;
		AfterValuesConnection<UserTask> connection=new AfterValuesConnection<>(env);
		//searchAfter模式的分页，这里不需要offset的，需要cursor： after+first。
		int limit=connection.getLimit();
		//Pageable pageable;		//zeebe TaskList：也没有排序字段，不用Sort.Direction.ASC
		//【局限】底层API的搜索过滤分组的能力较差！只能来什么就尽快处理什么，不支持长时间滞留UserTask的作业模式，不能像普通等待列表那样区分任务和差异化对待。
		//假如长时间无法处置当前任务的，在BPMN流程上附加一个等待黄晖环节节点：这次暂时退到中间状态节点，设置一个合理短暂挂起周期后再次流转回到原本环节给原来User再来处置一次机会。
		//List<String> search;如何跨越下一次http请求？分页？
		Pagination page=new Pagination.Builder().pageSize(limit).after(connection.getCursorValues()).build();
		TaskList jobs= userTaskListService.fetchMyUserTask(user.getUsername(),page);
        List<UserTask> todos = new ArrayList<UserTask>();
		if(null!=jobs && jobs.size()>0) {
			jobs.forEach(task -> {
				todos.add(new UserTask(task));
			});
		}
		//若读取到的个数少于分页大小的：就可认定是最后一页。

		return connection.setListData(todos).get(env);
	}
	/**前端看到的主报告 报告媒体形式：
	 * */
	@SchemaMapping(field="mdtype")
	public ReportType_Enum  mdtype(Isp isp, DataFetchingEnvironment env) {
		Report report=isp.getReport();
		return  (null==report? ReportType_Enum.URI : report.getType() );
	}
	/**前端看到的主报告 URI：给监察机构看的。
	 * */
	@SchemaMapping(field="mduri")
	public String  mduri(Isp isp, DataFetchingEnvironment env) {
		Report report=isp.getReport();
		String uri="";
		if(null==report){		//尽量提取报告链接地址
		//检验机构默认的旧平台对接获取：
			if(null==isp.getIspu() || "福建省特种设备检验研究院".equals(isp.getIspu().name())) {
				uri = queryFjtjRepCod(isp.getNo());
			}
		}else {
			uri = report.getPath();
		}
		return  uri;
	}
	/**[从特检院的旧平台]获取 旧平台的报告编号对应报告访问地址。
	 * 前端做这些请求，会遭遇第三方后端服务器的跨域问题困扰，所以只好挪到后端做处理（等同代理功能）。
	 依据IDEA的Http request工具界面才能比对出问题何方.  @后端非空参数没有校验报错.
	 使用该工具测定必须设置的所有参数:REPORT_COD_q=ZZ2021FTC02097&EQP_TYPE_q=&IF_REPGOBACK_q=&IS_SHOW_FILED_q=&QUERY_JSON=%5B%5D&pageIndex=0&pageSize=200
	 * */
	private String queryFjtjRepCod(String no){
		User user= checkAuth();
		if(user==null)   return null;
		String ppath="http://27.151.117.66:9922/fjsei/CollQuery/IspQuery/req.jsp?menuid=207&t=query_btn";

		MultiValueMap<String, String> paramMap = new LinkedMultiValueMap(){
			{
				add("REPORT_COD_q", no);
				add("EQP_TYPE_q","");
				add("IF_REPGOBACK_q","");
				add("IS_SHOW_FILED_q","");
				add("QUERY_JSON","[]");
				add("pageIndex","0");
				add("pageSize","200");
			}
		};
		//旧平台账户作为验证授权
		String cookies="FJSEINEW_COD="+user.getOldAccount();
		//不是一定要.setDefaultCookieStore(cookieStore),不保存会话状态模式; 对方可能采用额外的登录验证,token,cookies等;
		try {
			RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
					.post(new URI(""))
					.header(HttpHeaders.COOKIE, cookies)
					.contentType(MediaType.APPLICATION_FORM_URLENCODED)   //参数传输类型 url编码格式 application/x-www-form-urlencoded
					.accept(MediaType.TEXT_HTML).acceptCharset(StandardCharsets.UTF_8)
					.body(paramMap);
			ResponseEntity<String>  entity=restTemplate.postForEntity(ppath, requestEntity, String.class);
			//ResponseEntity<String>  entity=restTemplate.postForEntity(url, httpEntity, String.class);
			Assert.state(entity.getStatusCode()==HttpStatus.OK, "状态码!=200");
			Type typeclass= new TypeReference<ResponseADto<PostExampleDataDto>>(){}.getType();
			List<PostExampleDataDto> dataDto= convertModel(entity.getBody(), typeclass);
			if(dataDto.size()>0) {
				PostExampleDataDto hit = dataDto.get(0);        //最多只有一条
				return "http://27.151.117.66:9922/fjsei/upload/sesrep/" + hit.getDATA_PATH() + "/sealReport.pdf";
				// 有些是/main.xls + testlog.doc；
			}
		}
		catch (Exception e) {
			e.printStackTrace();
			//可能会重定向到登录界面 	entity.getBody(): "密码存在的话把“记住用户名和密码”复选框勾选住
			log.error("请求读取第三方报告: {} 失败", no);
			//throw new RuntimeException(message, e);
			return null;
		}
		return null;
	}

	/**针对服务端应答时通常的 { "data": [{,}] } 类型做提取对象.
	 * */
	public static  <T> List<T>  convertModel(String responseString, Type typeclazz){
		ResponseADto<T> responseDto= JSONObject.parseObject(responseString)
				.to(typeclazz);
		return responseDto.getData();
	}

}

